﻿using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Alphaleonis.Win32.Filesystem;
using Un4seen.Bass;
using FileNotFoundException = System.IO.FileNotFoundException;
using FileStream = System.IO.FileStream;

namespace CueMounter
{
	public class BassWaveProvider : IWaveProvider {
		private const long SkipDataThreshold = 1048576;
		public static readonly string[] SupportedAudioFiles = new string[] { ".ogg", ".wav", ".mp3", ".aac", ".ac3", ".m4a", ".ape", ".mpc", ".tta", ".flac", ".wma", ".wv" };

		private int id;
		private string fileName;
		private FileStream stream;
		private TagLib.File TagLibFile;
		private BASS_CHANNELINFO channelInfo;
		private long byteOffset, byteLength;

		static void PluginLoad(string plugin) {
			if(Bass.BASS_PluginLoad(plugin) == 0) {
				throw new FileNotFoundException(Bass.BASS_ErrorGetCode().ToString());
			}
		}
		static BassWaveProvider() {
			BassNet.Registration("ivailo91@gmail.com", "2X2917118152222");
			Bass.BASS_Init(0, 192000, BASSInit.BASS_DEVICE_NOSPEAKER, IntPtr.Zero);
			// Load Input Libs
			PluginLoad("bass_aac.dll");
			PluginLoad("bass_ac3.dll");
			PluginLoad("bass_alac.dll");
			PluginLoad("bass_ape.dll");
			PluginLoad("bass_mpc.dll");
			PluginLoad("bass_tta.dll");
			PluginLoad("bassflac.dll");
			PluginLoad("basswma.dll");
			PluginLoad("basswv.dll");
		}

		public BassWaveProvider(string fileName) {
			this.fileName = fileName;
			try {
				TagLibFile = TagLib.File.Create(fileName);
			} catch {
			}
			CreateStream(fileName);
			SetSpanBytes(0, 0);
		}

		public void SetSpanBytes(long byteOffset, long byteLength = 0) {
			this.byteOffset = byteOffset;
			if(byteLength == 0) {
				this.byteLength = Bass.BASS_ChannelGetLength(id, BASSMode.BASS_POS_BYTES) - this.byteOffset;
			} else {
				this.byteLength = byteLength;
			}
		}

		public void SetSpanFrames(long frameOffset, long frameLength = 0) {
			SetSpanBytes(FramesToBytes(frameOffset), FramesToBytes(frameLength));
		}

		private FILECLOSEPROC FileCloseDelegate;
		private void FileCloseProc(IntPtr user) {
			stream.Close();
		}

		private FILELENPROC FileLenDelegate;
		private long FileLenProc(IntPtr user) {
			return stream.Length;
		}

		private byte[] fileReadBuffer;
		private FILEREADPROC FileReadDelegate;
		private int FileReadProc(IntPtr buffer, int length, IntPtr user) {
			if(fileReadBuffer == null || fileReadBuffer.Length < length) {
				fileReadBuffer = new byte[length * 2];
			}
			try {
				int read = stream.Read(fileReadBuffer, 0, length);
				Marshal.Copy(fileReadBuffer, 0, buffer, read);
				return read;
			} catch {
				return 0;
			}
		}

		private FILESEEKPROC FileSeekDelegate;
		private bool FileSeekProc(long offset, IntPtr user) {
			try {
				stream.Position = offset;
				return true;
			} catch {
				return false;
			}
		}

		private void CreateStream(string fileName) {
			stream = File.OpenRead(fileName);
			FileCloseDelegate = new FILECLOSEPROC(FileCloseProc);
			FileLenDelegate = new FILELENPROC(FileLenProc);
			FileReadDelegate = new FILEREADPROC(FileReadProc);
			FileSeekDelegate = new FILESEEKPROC(FileSeekProc);
			BASS_FILEPROCS procs = new BASS_FILEPROCS(FileCloseDelegate, FileLenDelegate, FileReadDelegate, FileSeekDelegate);
			id = Bass.BASS_StreamCreateFileUser(BASSStreamSystem.STREAMFILE_NOBUFFER, BASSFlag.BASS_STREAM_DECODE | BASSFlag.BASS_STREAM_PRESCAN, procs, IntPtr.Zero);
			if(id == 0) {
				throw new Exception("BASS Error: " + Bass.BASS_ErrorGetCode());
			}
			channelInfo = Bass.BASS_ChannelGetInfo(id);
		}

		public void Dispose() {
			if(id != 0) {
				Bass.BASS_StreamFree(id);
				id = 0;
			}
		}

		public long Length {
			get {
				return byteLength;
			}
			set
			{
				throw new NotSupportedException();
			}
		}

		public int SampleRate {
			get {
				return channelInfo.freq;
			}
		}

		public int BitsPerSample {
			get {
				if(channelInfo.Is8bit) {
					return 8;
				} else if(channelInfo.Is32bit) {
					return 32;
				} else {
					return 16;
				}
			}
		}

		public int Channels {
			get {
				return channelInfo.chans;
			}
		}

		public int BytesPerSecond {
			get {
				return SampleRate * BitsPerSample * Channels / 8;
			}
		}

		public long BytesToSamples(long bytes) {
			return bytes * 8 / BitsPerSample / Channels;
		}

		public long FramesToBytes(long frames) {
			return frames * BytesPerSecond / 75;
		}

		private long SkipData(long count) {
			byte[] buffer = new byte[4096];
			long skipped = 0;
			while(skipped < count) {
				long toRead = Math.Min(buffer.Length, count - skipped);
				int read = Bass.BASS_ChannelGetData(id, buffer, (int)toRead);
				if(read <= 0) {
					return skipped;
				}
				skipped += read;
			}
			return skipped;
		}

		public bool CanRead { get { return true; } }
		public bool CanWrite { get { return false; } }

		public unsafe long Read(long streamOffset, byte[] buffer, long bufferOffset, long length) {
			long targetPos = byteOffset + streamOffset;
			long targetLen = Math.Min(byteLength - streamOffset, length);
			if(targetLen <= 0) {
				return 0;
			}
			long cpos = Bass.BASS_ChannelGetPosition(id, BASSMode.BASS_POS_BYTES);
			if(cpos != targetPos) {
				if(targetPos > cpos && targetPos - cpos < SkipDataThreshold) {
					Debug.WriteLine("Skip " + (targetPos - cpos));
					long toSkip = targetPos - cpos;
					if(SkipData(toSkip) != toSkip) {
						return 0;
					}
				} else {
					Debug.WriteLine("Seek " + fileName + " " + id + " cur " + cpos + " to " + targetPos + " delta " + (targetPos - cpos));
					Bass.BASS_ChannelSetPosition(id, targetPos, BASSMode.BASS_POS_BYTES);
				}
			}
			fixed(byte* bufferPtr = buffer) {
				return Bass.BASS_ChannelGetData(id, (IntPtr)(bufferPtr + bufferOffset), (int)targetLen);
			}
		}

		public TagLib.Tag Tags {
			get {
				return TagLibFile.Tag;
			}
		}

		public long StartBytes {
			get {
				return byteOffset;
			}
		}

		public long LengthBytes {
			get {
				return byteLength;
			}
		}

		public TimeSpan BytesToTime(long bytes) {
			return TimeSpan.FromSeconds((double)bytes / BytesPerSecond);
		}

		public long Write(long fileOffset, byte[] buffer, long bufferOffset, long length)
		{
			throw new NotSupportedException();
		}
	}
}